package dkbta.ontology.elements.abstractions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import dkbta.ontology.elements.Element;
import dkbta.ontology.elements.abstractions.interpolation.PersistenceFunction;
import dkbta.processing.Patient;
import dkbta.util.InstanceTime;
import dkbta.util.time.TimeInterval;

/**
 * A an abstract mapping function that operates on a single primitive and performs
 * abstractions between subsequent pairs of them. The primary mapping logic is left to the
 * extending class in the methods <tt>mapElements</tt> and <tt>calculateValue</tt>
 * 
 * @author Uri Kanonov
 * @see #mapElements(List, String, Element, Element, PersistenceFunction)
 */
public abstract class PairedMappingFunction<T extends Abstraction> {

	/**
	 * Goes over all of the instances of the given primitive obtained from the patient and
	 * attempts to abstract subsequent pairs such that both of them are under all of the
	 * necessary contexts
	 * 
	 * @param p The associated patient
	 * @param abstractions The list to which to add the created abstractions
	 * @param abstractionName The name of the abstraction that will be created
	 * @param primitiveName The name of the primitive the abstraction is abstracted from
	 * @param necessaryContexts The contexts necessary for the abstraction process
	 * @param interpolation The persistence function
	 */
	public void performMapping(Patient p, List<T> abstractions, String abstractionName,
		String primitiveName, String[] necessaryContexts,
		PersistenceFunction interpolation){
		List<InstanceTime> times = new ArrayList<InstanceTime>();

		// For each primitive adding its start to the times list
		// with the primitive itself. We add only the start time and
		// not both the start and end because they are the same for primitives
		List<? extends Element> elements = p.getPrimitives(primitiveName);
		for (Element e : elements){
			TimeInterval ti = e.getTimeInterval();
			times.add(new InstanceTime(e, -1, ti.getStartTime(), true));
		}
		for (String contextName : necessaryContexts){
			// For each context adding its start and end times to the times list
			// without the context instance. We will merely need to separate the
			// primitives from the contexts and will do it by checking
			// if the element field is null
			elements = p.getContexts(contextName);
			for (Element e : elements){
				TimeInterval ti = e.getTimeInterval();
				times.add(new InstanceTime(null, -1, ti.getStartTime(), true));
				times.add(new InstanceTime(null, -1, ti.getEndTime(), false));
			}
		}
		// Sorting the time list chronologically
		Collections.sort(times);

		// Going over the times to find all of the overlaps of all contexts and
		// each pair of primitives
		int currentlyOverlapping = 0;
		int numOfNC = necessaryContexts.length;
		Element previous = null;
		for (InstanceTime it : times){
			if (it.isStart){
				Element e = it.element;
				if (e == null){ // it's a context
					++currentlyOverlapping;
				}else{ // It's a primitive
					if (currentlyOverlapping == numOfNC){
						// We have all of the necessary contexts so now
						// we check if we have a previous primitive
						// to create the abstraction from
						if (previous != null){
							mapElements(abstractions, abstractionName, previous, e,
								interpolation);
						}
						previous = e;
					}
				}
			}else{
				--currentlyOverlapping;
				// There are no longer all of the necessary contexts over
				// the gap between the previous primitive (if there is one)
				// and the next so we forget the previous one
				previous = null;
			}
		}
	}

	/**
	 * Attempting to map the given two elements (before, after). The elements are
	 * guaranteed to be subsequent and both under all of the necessary contexts.
	 * 
	 * @param abstractions The list of abstractions so far
	 * @param abstractionName The name of the abstraction to be created
	 * @param before The earlier interval
	 * @param after The later interval
	 * @param interpolation The persistence function
	 */
	protected abstract void mapElements(List<T> abstractions, String abstractionName,
		Element before, Element after, PersistenceFunction interpolation);

	/**
	 * Mapping two subsequent elements into a value
	 * 
	 * @param before The earlier element
	 * @param after The later element
	 * @param interpolation The persistence function
	 * @return The mapped value if the elements could be mapped and null otherwise
	 */
	protected String mapPair(Element before, Element after,
		PersistenceFunction interpolation){
		double beforeValue = (Double)before.getValue();
		double afterValue = (Double)after.getValue();
		TimeInterval beforeTI = before.getTimeInterval();
		TimeInterval afterTI = after.getTimeInterval();
		return mapPair(beforeValue, afterValue, beforeTI, afterTI, interpolation);
	}

	/**
	 * Mapping two subsequent intervals (with their values) into a value. First the value
	 * variation per millisecond between the two is computed and a value is calculated
	 * using <tt>calculateValue(double variationPerMillisecond)</tt>. Secondly, if a
	 * persistence function was provided, the value is returned if the persist func allows
	 * the joining of the two intervals. If there is no persist func, the value is
	 * returned.
	 * 
	 * @param beforeValue The value of the earlier interval
	 * @param afterValue The value of the later interval
	 * @param beforeTI The earlier interval
	 * @param afterTI The later interval
	 * @param interpolation
	 * @return The mapped value if the intervals (with their values) could be mapped and
	 *         null otherwise
	 * @see #calculateValue(double)
	 */
	protected String mapPair(double beforeValue, double afterValue,
		TimeInterval beforeTI, TimeInterval afterTI, PersistenceFunction interpolation){
		long endTime = afterTI.getEndTime();
		long startTime = beforeTI.getStartTime();
		long timeDiff = endTime - startTime;

		// The value variation between the two primitives per millisecond
		double variation = (afterValue - beforeValue) / timeDiff;

		// Determining the value of the abstraction
		String value = calculateValue(variation);

		// Determining whether the abstraction can be created according
		// to the persistence table for its value
		return (value != null && (interpolation == null || interpolation.canBeJoined(
			value, beforeTI, afterTI))) ? value : null;
	}

	/**
	 * Calculates the value of the abstraction given the variation per millisecond
	 * 
	 * @param variationPerMillisecond The variation
	 * @return The value of the abstraction
	 */
	protected abstract String calculateValue(double variationPerMillisecond);
}
